//
//  MainGridStreamsVC.swift
//  Hermes
//
//  Created by FH on 2021/5/19.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

fileprivate class VideoCanvasView : FLCommonView {
    var hasRender = false
    var usedCustomRender = false
    var enableCustomRender = false
    var index: Int = -1
    var pageIndex: Int = 0
    var canvas: AgoraRtcVideoCanvas!
    var customCanvas: AgoraRenderView!
    var videoPanel = NSView()
    var coverView = NSView()
    var backgroundColor: NSColor {
        get { return self.videoPanel.flBackgroundColor }
        set { self.videoPanel.flBackgroundColor = newValue}
    }
    var uid: UInt {
        if let canvas = self.canvas {
            return canvas.uid
        }
        return 0
    }
    
    override func layoutUI(yoga: FLContext) {
        self.videoPanel.bind(to: yoga, self) { it in
            it.flexGrow = 1
            it.margin = 1
        }
    }
    
    func setupCanvas(uid: UInt, forLocal: Bool, customRender: Bool, _ appContext: HermesContext) {
        let succ = self.mutex.tryLock()
        if !self.hasRender {
            self.hasRender = true
            // handle mediaIO
            self.enableCustomRender = customRender
            if customRender {
                self.usedCustomRender = true
            }
            if customRender {
                let render = forLocal ? appContext.settings.mediaIOLocalRender : appContext.settings.mediaIORemoteRender
                self.customCanvas = AgoraRenderView()
                self.customCanvas.uid = uid
                self.customCanvas.type = AgoraVideoBufferType(rawValue: render.bufferType)!
                self.customCanvas.format = AgoraVideoPixelFormat(rawValue: render.pixelFormat)!
                self.customCanvas.frame = CGRect(x: 0, y: 0,
                                                 width: self.videoPanel.bounds.width,
                                                 height: self.videoPanel.bounds.height)
                self.videoPanel.addSubview(self.customCanvas)
            } else {
                self.canvas = AgoraRtcVideoCanvas()
                canvas.renderMode = appContext.settings.videoRenderMode
                canvas.uid = uid
                canvas.view = self.videoPanel
            }
            if forLocal {
                if customRender {
                    appContext.engine.invoke("setLocalVideoRenderer:", self.customCanvas)
                } else {
                    if self.usedCustomRender {
                        appContext.engine.invoke("setLocalVideoRenderer:", AgoraRtcDefaultRenderer(view: self.canvas.view!, renderMode: self.canvas.renderMode))
                    }
                    appContext.engine.invoke("setupLocalVideo:", self.canvas)
                }
            } else {
                if customRender {
                    appContext.engine.invoke("setRemoteVideoRenderer:forUserId:", self.customCanvas, uid)
                } else {
                    appContext.engine.invoke("setupRemoteVideo:", self.canvas)
                }
            }
        }
        if succ {
            self.mutex.unlock()
        }
    }
    
    func removeVideoRenderView(forLocal: Bool, _ appContext: HermesContext) {
        let succ = self.mutex.tryLock()
        if self.hasRender {
            // remove AgoraRtcVideoCanvas's renderView
            if let renderView = self.videoPanel.subviews.first {
                renderView.removeFromSuperview()
            }
            if forLocal {
                if self.enableCustomRender {
                    appContext.engine.invoke("setLocalVideoRenderer:", nil)
                } else {
                    appContext.engine.invoke("setupLocalVideo", nil)
                }
            } else {
                if self.enableCustomRender {
                    appContext.engine.invoke("setRemoteVideoRenderer:forUserId:", nil, uid)
                } else {
                    appContext.engine.invoke("setupRemoteVideo", nil)
                }
            }
            self.canvas = nil
            self.customCanvas = nil
            self.index = -1
            self.pageIndex = 0
            self.enableCustomRender = false
            self.hasRender = false
        }
        if succ {
            self.mutex.unlock()
        }
    }
}

extension VideoCanvasView {
    override var description: String {
        let origin = self.frame.origin
        let size = self.frame.size
        return "(\(self.hasRender ? "hasRender" : "notRender"))item: \(self.index), uid: \(self.uid), frame: {\(origin.x), \(origin.y), \(size.width), \(size.height)}"
    }
}

// handle mediaIO delegate

extension MainGridStreamsPanelVC : AudioCaptureSessionCenterDelegate {
    func audioCaptureSessionCenter(center: AudioCaptureSessionCenter, didCaptureBuffer buffer: CMSampleBuffer) {
        self.appContext.agoraSDK?.pushExternalAudioFrameSampleBuffer(buffer)
    }
}

//////////////////////////////////////////////////

let normalBodyWidth: CGFloat = 1432
let remoteListWidth: CGFloat = 132
let halfBodyWidth: CGFloat = 648
let halfBodyMargin: CGFloat = 2

class MainGridStreamsPanelVC : FLViewController {
    private let appContext = HermesContext()
    private let headerView = NSView()
    private let bodyView = NSView()
    private let listView = NSView()
    private let leftBodyView = NSView()
    private let rightBodyView = NSView()
    // header
    private let inputGridCount = FLInputField()
    private let selectLayoutType = NSPopUpButton()
    private let selectPageSize = NSPopUpButton()
    private let btnNextPage = NSButton()
    private let btnPreviousPage = NSButton()
    private let btnSaveSettings = NSButton()
    private let chkUseAspectFill = FLCheckbox()
    private let btnFullScreen = NSButton()
    private let btnMoreSettings = NSButton()
    // body
    private let smallLocalView = VideoCanvasView()
    private let bigStreamView = VideoCanvasView()
    private var smallStreamViews = [VideoCanvasView]()
    private var pageViews = [NSView]()
    // inner states
    private let mutex = Mutex()
    private var pageIndex = 0
    private var smallLocalId: UInt = 0
    private var bigStreamId: UInt = 0
    private var currRemoteIndex = 0
    private var streamIdMapping: [UInt] = Array(repeating: 0, count: 64)
    private var needLayoutByManual = false
    private var needLayoutOnDidLayout = false
    // present grid panel to fullScreen
    private var useFullScreenMode = false
    private var cellCountMapping = [Int : Int]()
    private var cellSizeMapping = [Int : CGSize]()
    private var gridCountMapping = [Int : Int]()
    private var cachedPageSizes = [Int]()
    private var cachedRemoteIndex = Array(repeating: 0, count: 130)
    private var containerSize = CGSize.zero
    private var layoutContext: [String : Any?]? = nil
    // for mediaIO
    private var enableAudioSource = false
    private var enableVideoSource = false
    private var externalAudioSession: AudioCaptureSessionCenter?
    private let sdkCamera = AgoraRtcDefaultCamera()
    
    private func dataBinding(_ needsLayout: Bool) {
        if selectLayoutType.itemTitles.count == 0 {
            self.startMonitorRender()
            self.getCellCountMapping()
            self.selectLayoutType.addItems(withTitles: ["Fixed", "Paging", "Scrolling"])
            self.appContext.preferences.$gridCount.bindTo(self.inputGridCount)
            self.appContext.preferences.$layoutType.bindTo(self.selectLayoutType)
            self.appContext.preferences.$useAspectFill.bindTo(self.chkUseAspectFill)
            RACAssign(self.appContext.preferences).gridCount = self.btnSaveSettings.rac_eventTouch
                .map {[weak self] _ in
                    if let self = self, let gridCount = Int(self.inputGridCount.flText),
                       gridCount != self.appContext.preferences.gridCount {
                        if self.layoutContext != nil {
                            self.layoutContext!.removeValue(forKey: "gridCount")
                            if self.layoutContext!.count == 0 {
                                self.layoutContext = nil
                            }
                        } else {
                            self.needLayoutByManual = true
                        }
                        return gridCount
                    }
                    return -1
                }.filter { $0 > -1 }
            RACAssign(self.appContext.preferences).pageSize = self.btnSaveSettings.rac_eventTouch
                .map {[weak self] _ in
                    var result = -1
                    if let self = self, let pageSize = Int(self.selectPageSize.selectedItemTitle),
                       pageSize > 0, pageSize != self.appContext.preferences.pageSize {
                        if self.layoutContext != nil {
                            self.layoutContext!.removeValue(forKey: "pageSize")
                            if self.layoutContext!.count == 0 {
                                self.layoutContext = nil
                            }
                        } else {
                            self.needLayoutByManual = true
                        }
                        result = pageSize
                    }
                    return result
                }.filter { $0 > -1 }
            RACAssign(self.appContext.preferences).useAspectFill = self.btnSaveSettings.rac_eventTouch
                .map {[weak self] _ -> Int in
                    if let self = self {
                        let oldState = self.appContext.preferences.useAspectFill
                        let currState = self.chkUseAspectFill.state == .on
                        if oldState != currState {
                            if self.layoutContext != nil {
                                self.layoutContext!.removeValue(forKey: "useAspectFill")
                                if self.layoutContext!.count == 0 {
                                    self.layoutContext = nil
                                }
                            } else {
                                self.needLayoutByManual = true
                            }
                            return self.chkUseAspectFill.state.rawValue
                        }
                    }
                    return -1
                }.filter { $0 > -1 }.map { $0 == 1 ? true : false }

            self.btnSaveSettings.rac_eventTouch.subscribeNext {[weak self] _ in
                if let self = self {
                    let useAspectFill = self.chkUseAspectFill.state == .on
                    if let gridCount = Int(self.inputGridCount.flText), let pageSize = Int(self.selectPageSize.selectedItemTitle),
                       self.needLayoutByManual || useAspectFill != self.appContext.preferences.useAspectFill
                        || gridCount != self.appContext.preferences.gridCount
                        || self.smallStreamViews.count != self.appContext.preferences.gridCount
                        || pageSize != self.appContext.preferences.pageSize {
                        self.needLayoutByManual = false
                        var context = [String : Any?]()
                        // btn event was triggered before the RAC(state)
                        if !self.needLayoutByManual {
                            if useAspectFill != self.appContext.preferences.useAspectFill {
                                context["useAspectFill"] = useAspectFill
                            }
                            if gridCount != self.appContext.preferences.gridCount {
                                context["gridCount"] = gridCount
                            }
                            if pageSize != self.appContext.preferences.pageSize {
                                context["pageSize"] = pageSize
                            }
                            self.layoutContext = context
                        }
                        self.refreshStreamViews(useAspectFill, context)
                    } else {
                        print("not >>> useAspectFill: \(useAspectFill), grid: \(self.inputGridCount.flText)")
                    }
                }
            }
            // handle render canvas view
            self.appContext.mainStates.$handleStreamTrigger.signal().subscribeNext {[weak self] arg in
                if let self = self, let arg = arg {
                    let succ = self.mutex.tryLock()
                    let useAspectFill = self.appContext.preferences.useAspectFill
                    let notRemoveWhenOffline = self.appContext.preferences.notRemoveWhenOffline
                    let uid = arg.0
                    let forRemote = arg.1
                    let forSetup = arg.2
                    var index = 0
                    // handle index of streamId
                    if forSetup {
                        // local stream's index is always 0
                        if forRemote {
                            self.currRemoteIndex += 1
                            index = self.currRemoteIndex
                        }
                        if index < self.streamIdMapping.count {
                            self.streamIdMapping[index] = uid
                        } else {
                            self.streamIdMapping.append(uid)
                        }
                    } else {
                        if uid == UInt.max {
                            let range = 1..<self.streamIdMapping.count
                            self.streamIdMapping.replaceSubrange(range, with: Array(repeating: UInt(0),
                                                                                    count: self.streamIdMapping.count - 1))
                            self.currRemoteIndex = 0

                        } else {
                            if forRemote && !notRemoveWhenOffline {
                                self.currRemoteIndex -= 1
                            }
                            if let index = self.streamIdMapping.firstIndex(where: { $0 == uid }) {
                                self.streamIdMapping[index] = 0
                            }
                        }
                    }
                    if self.needRefreshStreams(forSetup, forRemote, useAspectFill, self.currRemoteIndex) {
                        self.refresh()
                    } else {
                        self.renderStreamView(forSetup, forRemote, useAspectFill, uid)
                    }
                    if succ {
                        self.mutex.unlock()
                    }
                }
            }
            // observer joinChannel
            self.appContext.mainStates.$hasJoined.signal().skip(1).subscribeNext {[weak self] join in
                if let self = self {
                    if !join {
                        // source audio
                        if self.enableAudioSource {
                            self.appContext.engine.invoke("disableExternalAudioSource")
                            self.externalAudioSession?.stopCapture()
                            self.externalAudioSession?.deallocCapture()
                            self.externalAudioSession = nil
                            self.enableAudioSource = false
                        }
                        // source video
                        if self.enableVideoSource {
                            self.appContext.agoraSDK?.setVideoSource(self.sdkCamera)
                            self.enableVideoSource = false
                        }
                    }
                    print(">>> TODO: remove, reset pageIndex、cells when gridCount == 0")
                }
            }
            // handle mediaIO
            self.appContext.mainStates.$willJoinChannel.signal().filter({ $0 == true })
                .subscribeNext {[weak self] _ in
                if let self = self {
                    let enableAudio = self.appContext.settings.enableAudioSource
                    let enableVideo = self.appContext.settings.enableVideoSource
                    if enableAudio || enableVideo {
                        let succ = self.mutex.tryLock()
                        // source audio
                        if !self.enableAudioSource && enableAudio {
                            self.enableAudioSource = true
                            let externalAudioSession = AudioCaptureSessionCenter(delegate: self)
                            let sampleRate = UInt(self.appContext.settings.externalAudioSource.sampleRate)
                            let numberOfChannels = UInt(self.appContext.settings.externalAudioSource.channelsPerFrame)
                            if externalAudioSession.setupCapture(sampleRate:sampleRate,
                                                                 numberOfChannels:numberOfChannels) {
                                self.appContext.engine
                                    .invoke("enableExternalAudioSourceWithSampleRate:channelsPerFrame:",
                                            sampleRate, numberOfChannels)
                                externalAudioSession.startCapture()
                                __logWrite("[MediaIO] setup external audio success!")
                            } else {
                                __logWrite("[MediaIO] setup external audio falied!")
                            }
                            self.externalAudioSession = externalAudioSession
                        }
                        // source video
                        if !self.enableVideoSource && enableVideo {
                            self.enableVideoSource = true
                            let customCamera = AgoraCamera()
                            customCamera.type = AgoraVideoBufferType(rawValue: self.appContext.settings.mediaIOVideoSource.bufferType)!
                            customCamera.format = AgoraVideoPixelFormat(rawValue: self.appContext.settings.mediaIOVideoSource.pixelFormat)!
                            self.appContext.agoraSDK?.setVideoSource(customCamera)
                        }
                        if succ {
                            self.mutex.unlock()
                        }
                    }
                }
            }
        }
    }
    
    // REMARK: handle index of streamIds
    
    private func needRefreshStreams(_ forSetup: Bool, _ forRemote: Bool, _ useAspectFill: Bool, _ currRemoteIndex: Int) -> Bool {
        let localStreamId = self.appContext.mainStates.streamsInfo.localStreamId
        let smallCellsCount = self.smallStreamViews.count + (useAspectFill ? 1 : 0)
        if forSetup {
            if forRemote {
                return currRemoteIndex > smallCellsCount || smallCellsCount > self.smallStreamViews.count
            } else {
                let flag1 = useAspectFill && ((self.smallLocalId > 0 || (self.bigStreamId > 0 && self.bigStreamId == localStreamId))
                                                || self.smallStreamViews.count == 0)
                let flag2 = !useAspectFill && (self.bigStreamId == 0 && self.bigStreamView.bounds == CGRect.zero)
                return flag1 || flag2
            }
        } else if forRemote && self.appContext.preferences.gridCount == 0
                    && !self.appContext.preferences.notRemoveWhenOffline
                    && (!useAspectFill && self.currRemoteIndex < self.smallStreamViews.count) {
            return true
        }
        return false
    }
    
    private func refreshStreamViews(_ useAspectFill: Bool, _ context: [String : Any?]) {
        let succ = mutex.tryLock()
        let localStreamId = self.appContext.mainStates.streamsInfo.localStreamId
        let smallLocalId = self.smallLocalId
        var bigStreamId = self.bigStreamId
        if !useAspectFill && self.smallStreamViews.count > 0 && self.smallStreamViews[0].uid == localStreamId {
            bigStreamId = localStreamId
        }
        if self.appContext.mainStates.streamsInfo.remoteStreamIds.count > 0 {
            self.renderStreamView(false, true, useAspectFill, UInt.max)
        }
        if localStreamId > 0 {
            self.renderStreamView(false, false, useAspectFill, localStreamId)
        }
        if !useAspectFill {
            self.smallLocalId = smallLocalId
            self.bigStreamId = bigStreamId
        }
        // refresh cells
        self.refresh(args: context)
        if succ {
            mutex.unlock()
        }
    }
    
    private func renderStreamView(_ forSetup: Bool, _ forRemote: Bool, _ useAspectFill: Bool, _ streamId: UInt) {
        if self.appContext.engine.isRunning {
            if forSetup {
                if forRemote {
                    if self.bigStreamId == streamId {
                        self.bigStreamView.setupCanvas(uid: streamId, forLocal: false,
                                                       customRender: self.appContext.settings.enableRemoteRender,
                                                       self.appContext)
                    } else {
                        for i in 1..<self.streamIdMapping.count {
                            let uid = self.streamIdMapping[i]
                            if uid == streamId {
                                let index = useAspectFill ? i : i - 1
                                let smallView = self.smallStreamViews[index]
                                smallView.setupCanvas(uid: streamId, forLocal: false,
                                                      customRender: self.appContext.settings.enableRemoteRender,
                                                      self.appContext)
                                break
                            }
                        }
                    }
                } else {
                    if useAspectFill {
                        self.smallStreamViews[0].setupCanvas(uid: streamId, forLocal: true,
                                                             customRender:self.appContext.settings.enableLocalRender,
                                                             self.appContext)
                    } else {
                        if self.bigStreamId == 0 || self.bigStreamId == streamId {
                            self.bigStreamId = streamId
                            self.bigStreamView.setupCanvas(uid: streamId, forLocal: true,
                                                           customRender: self.appContext.settings.enableLocalRender,
                                                           self.appContext)
                        } else if self.smallLocalId == streamId {
                            self.smallLocalView.setupCanvas(uid: streamId, forLocal: true,
                                                            customRender: self.appContext.settings.enableLocalRender,
                                                            self.appContext)
                        }
                    }
                }
            } else {
                if streamId == UInt.max {
                    let startIndex = useAspectFill ? 1 : 0
                    for i in startIndex..<self.smallStreamViews.count {
                        let smallView = self.smallStreamViews[i]
                        smallView.removeVideoRenderView(forLocal: false, self.appContext)
                    }
                    if self.bigStreamId > 0 && self.bigStreamId != self.appContext.mainStates.streamsInfo.localStreamId {
                        self.bigStreamId = 0
                        self.bigStreamView.removeVideoRenderView(forLocal: false, self.appContext)
                    }
                } else {
                    if forRemote {
                        if self.bigStreamId == streamId {
                            self.bigStreamId = 0
                            self.bigStreamView.removeVideoRenderView(forLocal: false, self.appContext)
                        } else {
                            for cell in self.smallStreamViews {
                                if cell.uid == streamId {
                                    cell.removeVideoRenderView(forLocal: false, self.appContext)
                                    break
                                }
                            }
                        }
                    } else {
                        if self.smallLocalId > 0 {
                            self.smallLocalId = 0
                            self.smallLocalView.removeVideoRenderView(forLocal: true, self.appContext)
                        } else if self.bigStreamId == streamId {
                            self.bigStreamId = 0
                            self.bigStreamView.removeVideoRenderView(forLocal: true, self.appContext)
                        } else if self.smallStreamViews[0].hasRender && self.smallStreamViews[0].uid == streamId {
                            self.smallStreamViews[0].removeVideoRenderView(forLocal: true, self.appContext)
                        }
                    }
                }
            }
        }
    }
    
    // REMARK: layout UI
    
    override func didLayoutUI() {
        var useAspectFill = self.appContext.preferences.useAspectFill
        if let context = self.layoutContext, let use = context["useAspectFill"] as? Bool {
            useAspectFill = use
        }
        let containerSize = self.getAreaSize(useAspectFill)
        var isPageRefresh = self.containerSize != CGSize.zero && containerSize != self.containerSize
        // get grid count and size
        if isPageRefresh || self.gridCountMapping.count == 0 {
            self.containerSize = containerSize
            self.getCellSizeMapping(self.containerSize)
        }
        if isPageRefresh || self.needLayoutOnDidLayout {
            // TODO: 设置发生变化也要做重置，container回到第一页操作。check remoteIndex没有位置需要page才会refresh.如果是新翻页则不需要removeAll()
            // 当前使用removeCell的形式，如果每次render视频性能不好，改为复用cell但要处理pageIndex
            if isPageRefresh {
                self.pageIndex = 0
                isPageRefresh = false
            }
            self.needLayoutOnDidLayout = false
            self.refresh()
        } else {
            // render stream views
            if self.appContext.engine.isRunning {
                let localStreamId = self.appContext.mainStates.streamsInfo.localStreamId
                if self.smallLocalId > 0 && !self.smallLocalView.hasRender {
                    self.smallLocalView.setupCanvas(uid: localStreamId, forLocal: true,
                                                    customRender: self.appContext.settings.enableLocalRender,
                                                    self.appContext)
                } else if self.bigStreamId > 0 && !self.bigStreamView.hasRender {
                    let forLocal = self.bigStreamId == localStreamId
                    let customRender = forLocal ? self.appContext.settings.enableLocalRender : self.appContext.settings.enableRemoteRender
                    self.bigStreamView.setupCanvas(uid: self.bigStreamId, forLocal: forLocal,
                                                   customRender: customRender, self.appContext)
                } else if !useAspectFill && self.bigStreamId == 0 && localStreamId > 0 {
                    self.bigStreamView.setupCanvas(uid: localStreamId, forLocal: true,
                                                   customRender: self.appContext.settings.enableLocalRender,
                                                   self.appContext)
                }
                let count = self.smallStreamViews.count
                if count > 0 {
                    let streamIds = Array(useAspectFill ? self.streamIdMapping[0...] : self.streamIdMapping[1...])
                    for i in 0..<count {
                        let streamId = streamIds[i]
                        let view = self.smallStreamViews[i]
                        if !view.hasRender {
                            let forLocal = i == 0 && useAspectFill
                            let customRender = forLocal ? self.appContext.settings.enableLocalRender : self.appContext.settings.enableRemoteRender
                            view.setupCanvas(uid: streamId, forLocal: forLocal,
                                             customRender: customRender, self.appContext)
                        }
                    }
                }
            }
        }
    }
    
    override func layoutUI(needsLayout: Bool, args: [String : Any?]? = nil) {
        self.dataBinding(needsLayout)
        self.layoutHeaderView(needsLayout)
        self.layoutBodyView(needsLayout)
        self.layoutGridView(needsLayout, context: args ?? [:])
    }
    
    private func layoutHeaderView(_ needsLayout: Bool) {
        self.headerView.bind(to: yoga, self.view) { it in
            it.direction = .row
            it.alignItems = .center
            it.height = 30
            it.marginTop = 4
            it.marginHorizontal = 4
        }
        self.headerView.flBackgroundColor = .lightGray
        // grid count
        let lblGridCount = FLLabel()
        lblGridCount.bind(to: yoga, self.headerView) { it in
            it.height = 20
        }
        lblGridCount.flText = "GridCount: "
        lblGridCount.textColor = .black
        lblGridCount.font = NSFont.systemFont(ofSize: 12)
        self.inputGridCount.bind(to: yoga, self.headerView) { it in
            it.width = 100
            it.height = 24
        }
        // layout type
        let lblLayoutType = FLLabel()
        lblLayoutType.bind(to: yoga, self.headerView) { it in
            it.height = 20
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        lblLayoutType.flText = "LayoutType: "
        lblLayoutType.textColor = .black
        lblLayoutType.font = NSFont.systemFont(ofSize: 12)
        self.selectLayoutType.bind(to: yoga, self.headerView) { it in
            it.width = 100
            it.height = 24
            // TODO: remove
            it.display = .none
        }
        // page size
        let lblPageSize = FLLabel()
        lblPageSize.bind(to: yoga, self.headerView) { it in
            it.height = 20
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        lblPageSize.flText = "PageSize: "
        lblPageSize.textColor = .black
        lblPageSize.font = NSFont.systemFont(ofSize: 12)
        self.selectPageSize.bind(to: yoga, self.headerView) { it in
            it.width = 100
            it.height = 24
            // TODO: remove
            it.display = .none
        }
        // buttons
        self.btnNextPage.bind(to: yoga, self.headerView) { it in
            it.width = 80
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.btnNextPage.flTitle = "Next Page"
        self.btnNextPage.flTitleColor = .white
        self.btnNextPage.flBackgroundColor = .gray
        self.btnNextPage.flCornerRadius = 4
        
        self.btnPreviousPage.bind(to: yoga, self.headerView) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.btnPreviousPage.flTitle = "Previous Page"
        self.btnPreviousPage.flTitleColor = .white
        self.btnPreviousPage.flBackgroundColor = .gray
        self.btnPreviousPage.flCornerRadius = 4
        
        self.btnSaveSettings.bind(to: yoga, self.headerView) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
        }
        self.btnSaveSettings.flTitle = "Update Layout"
        self.btnSaveSettings.flTitleColor = .white
        self.btnSaveSettings.flBackgroundColor = .gray
        self.btnSaveSettings.flCornerRadius = 4
        
        // functional area
        let functionalSpan = NSView()
        functionalSpan.bind(to: yoga, self.headerView) { it in
            it.position = .absolute
            it.direction = .row
            it.alignItems = .center
            it.right = 0
        }
        // support exchange
        self.chkUseAspectFill.bind(to: yoga, functionalSpan) { it in
            it.height = 20
        }
        self.chkUseAspectFill.font = NSFont.systemFont(ofSize: 12)
        self.chkUseAspectFill.flTitle = "Use AspectFill"
        // change to fullscreen
        self.btnFullScreen.bind(to: yoga, functionalSpan) { it in
            it.width = 120
            it.height = 26
            it.marginLeft = 8
            // TODO: remove
            it.display = .none
        }
        self.btnFullScreen.flTitle = "Open as FullScreen"
        self.btnFullScreen.flTitleColor = .white
        self.btnFullScreen.flBackgroundColor = .gray
        self.btnFullScreen.flCornerRadius = 4
        // more settings
        self.btnMoreSettings.bind(to: yoga, functionalSpan) { it in
            it.width = 90
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.btnMoreSettings.flTitle = "More Settings"
        self.btnMoreSettings.flTitleColor = .white
        self.btnMoreSettings.flBackgroundColor = .gray
        self.btnMoreSettings.flCornerRadius = 4
    }
    
    private func layoutBodyView(_ needsLayout: Bool) {
        // body view
        self.bodyView.bind(to: yoga, self.view) { it in
            it.direction = .row
            it.flexGrow = 1
            it.marginHorizontal = 4
        }
        self.listView.bind(to: yoga, self.bodyView) { it in
            it.width = remoteListWidth.toFLValue()
        }
    }
    
    private func layoutGridView(_ needsLayout: Bool, context: [String : Any?]) {
        var gridCount = self.appContext.preferences.gridCount
        if let count = context["gridCount"] as? Int {
            gridCount = count
        }
        var pageSize = self.appContext.preferences.pageSize
        if let size = context["pageSize"] as? Int {
            pageSize = size
        }
        var useAspectFill = self.appContext.preferences.useAspectFill
        if let use = context["useAspectFill"] as? Bool {
            useAspectFill = use
        }
        if self.containerSize.height > 0 {
            let containerSize = self.getAreaSize(useAspectFill)
            // update mapping size
            if containerSize != self.containerSize {
                self.getCellSizeMapping(containerSize)
                self.containerSize = containerSize
            }
        }
        // left body
        self.leftBodyView.bind(to: yoga, self.bodyView) { it in
            it.justify = .center
            if useAspectFill {
                it.display = .none
            } else {
                it.display = .flex
                if gridCount > 0 || self.currRemoteIndex > 0 {
                    it.width = halfBodyWidth.toFLValue()
                } else {
                    it.flexGrow = 1
                    it.marginRight = remoteListWidth.toFLValue()
                }
            }
        }
        self.bigStreamView.bindLayout(to: yoga, self.leftBodyView) { it in
            it.display = useAspectFill ? .none : .flex
            it.width = halfBodyWidth.toFLValue()
            it.flexGrow = 1
            if gridCount > 0 || self.currRemoteIndex > 0 {
                it.marginLeft = 0
            } else {
                it.marginLeft = ((normalBodyWidth - halfBodyWidth) / 2 - remoteListWidth).toFLValue()
            }
        }
        self.bigStreamView.backgroundColor = .darkGray
        // right body
        let rightBodyNode = self.rightBodyView.bind(to: yoga, self.bodyView) { it in
            it.direction = .row
            it.wrap = .wrap
            if useAspectFill {
                it.display = .flex
                // TODO: support fullscreen
                it.flexGrow = normalBodyWidth.toFLValue()
            } else {
                it.display = (gridCount > 0 || self.currRemoteIndex > 0) ? .flex : .none
                it.width = halfBodyWidth.toFLValue()
            }
        }
        // layout cell list
        self.smallStreamViews.removeAll()
        if gridCount == 0 && self.currRemoteIndex == 0
                    && self.appContext.preferences.cellHeight == 0 {
            if useAspectFill {
                rightBodyNode.wrap = .noWrap
                let cell = VideoCanvasView()
                cell.bindLayout(to: yoga, self.rightBodyView) { it in
                    it.width = (self.appContext.preferences.cellWidth > 0
                                    ? min(halfBodyWidth, self.appContext.preferences.cellWidth)
                                    : halfBodyWidth).toFLValue()
                    if gridCount == 0 || self.currRemoteIndex == 0 {
                        it.marginLeft = ((normalBodyWidth - halfBodyWidth) / 2 - remoteListWidth).toFLValue()
                    }
                }
                cell.backgroundColor = .darkGray
                self.smallStreamViews.append(cell)
            }
        } else if self.containerSize.height > 0 {
            var layoutCount = gridCount
            if layoutCount == 0 {
                layoutCount = self.appContext.mainStates.streamsInfo.totalStreamsCount(useAspectFill)
            }
            let cellSize = CGSize(width: self.appContext.preferences.cellWidth, height: self.appContext.preferences.cellHeight)
                var (width, height, totalCount) = self.checkCellSize(self.appContext.preferences.layoutType,
                                                                     useAspectFill, layoutCount, pageSize,
                                                                     self.pageIndex, containerSize)
            if cellSize.width > 0 {
                width = min(width, cellSize.width)
            }
            if cellSize.height > 0 {
                height = min(height, cellSize.height)
            }
            for i in 0..<totalCount {
                let cell = VideoCanvasView()
                cell.index = i + self.appContext.preferences.pageSize * self.pageIndex
                cell.pageIndex = self.pageIndex
                cell.bindLayout(to: yoga, self.rightBodyView) { it in
                    it.width = width.toFLValue()
                    it.height = height.toFLValue()
                }
                cell.backgroundColor = .lightGray
                if i == 0 && useAspectFill {
                    cell.backgroundColor = .darkGray
                }
                self.smallStreamViews.append(cell)
                cell.rac_observerEvent(.leftMouseDoubleDown).subscribeNext {[weak self] (cell, _) in
                    if let self = self {
                        // TODO: change to big stream
                    }
                }
            }
        } else {
            self.needLayoutOnDidLayout = true
            return
        }
    }
    
    // REMARK: help methods of calculate cell size

    private func checkCellSize(_ layoutType: AppGridLayoutType, _ useAspectFill: Bool, _ count: Int,
                               _ pageSize: Int, _ pageIndex: Int, _ areaSize: CGSize) -> (CGFloat, CGFloat, Int) {
        var cellCount = count
        if layoutType != .Fixed {
            cellCount = pageSize
        } else if useAspectFill {
            cellCount += 1
        }
        var cellSize: CGSize! = nil
        var totalCount: Int = 0
        if self.gridCountMapping.count == 0 && self.cellSizeMapping.count == 0 {
            self.getCellSizeMapping(areaSize)
        }
        if let layoutCount = self.gridCountMapping[cellCount], let size = self.cellSizeMapping[layoutCount] {
            cellSize = size
            totalCount = layoutCount
        }
        return (cellSize.width, cellSize.height, totalCount)
    }
    
    private func calculateAspectFill(_ cellCount: Int, _ layoutCount: Int, _ areaSize: CGSize) -> (CGSize, Int) {
        if cellCount == 1 {
            return (areaSize, 1)
        } else {
            // get square dim
            let (dim, rowNum) = self.calculateSquareDim(totalCount: layoutCount, areaSize: areaSize)
            var columnNum: CGFloat = CGFloat(layoutCount / Int(rowNum))
            if Int(rowNum * columnNum) < cellCount {
                columnNum += 1
            }
            var remainDim = areaSize.width - dim * columnNum
            let width = dim + remainDim / columnNum
            remainDim = areaSize.height - dim * rowNum
            let height = dim + remainDim / rowNum
            return (CGSize(width: width, height: height), Int(columnNum * rowNum))
        }
    }
    
    private func calculateSquareDim(totalCount: Int, areaSize: CGSize) -> (CGFloat, CGFloat) {
        let sizeWidth = areaSize.width
        let sizeHeight = areaSize.height
        let isMainAxisWidth = sizeWidth >= sizeHeight
        let mainAxisDim = isMainAxisWidth ? sizeWidth : sizeHeight
        let crossAxisDim = isMainAxisWidth ? sizeHeight : sizeWidth
        let mainAxisExtraDim = CGFloat(isMainAxisWidth
                                        ? constVideoItemMarginDim
                                        : (constRemoteUidHeight + constVideoItemMarginDim))
        let crossAxisExtraDim = CGFloat(isMainAxisWidth
                                        ? (constRemoteUidHeight + constVideoItemMarginDim)
                                        : constVideoItemMarginDim)
        if totalCount == 1 {
            let width = sizeWidth - mainAxisExtraDim
            let height = sizeHeight - crossAxisExtraDim
            return (width > height ? height : width, 1)
        }
        
        let remainDim = mainAxisDim - crossAxisDim
        // row/column基本值
        let number = CGFloat(Int(sqrt(Double(totalCount))))
        // row/column会多出来的cell数量
        var addMoreNumber = CGFloat(Int(number * remainDim / crossAxisDim))
        
        var dim: CGFloat = 0
        var currCrossNum: CGFloat = 0
        var isDecrease = true
        while true {
            let currNum = number + addMoreNumber
            var currDim = (mainAxisDim - currNum * mainAxisExtraDim) / currNum
            // 四舍五入处理，防止cell数量太多时，因为dim小数部分累加后导致一行留空太多
            let integerDim = CGFloat(Int(currDim))
            var decimalDim = currDim - integerDim
            if decimalDim > 0.5 || decimalDim < 0.4 {
                decimalDim = CGFloat(floor(Double(decimalDim * 10)) / 10)
                currDim = integerDim + decimalDim
            } else if decimalDim >= 0.4 {
                currDim = integerDim + 0.35
            }
            currCrossNum = CGFloat(Int(sizeHeight / (currDim + crossAxisExtraDim)))
            let currTotalNum = currNum * currCrossNum
            // 判断是要减少dim还是增加dim，以达到尽量填充width且不超出height的目的
            if currTotalNum >= CGFloat(totalCount) {
                dim = currDim
                if !isDecrease {
                    break
                }
            } else {
                if dim == 0 {
                    isDecrease = false
                }
                if isDecrease {
                    break
                }
            }
            if isDecrease {
                addMoreNumber -= 1
            } else {
                addMoreNumber += 1
            }
        }
        return (dim, CGFloat(Int(currCrossNum)))
    }
    
    private func getCellCountMapping() {
        self.cellCountMapping[1] = 1
        for i in 2...129 {
            var value = i
            while value.isPrime() {
                value += 1
            }
            self.cellCountMapping[i] = value
        }
    }
    
    private func getAreaSize(_ useAspectFill: Bool) -> CGSize {
        if self.useFullScreenMode, let screen = self.view.window?.screen {
            return screen.frame.size
        }
        return CGSize(width: useAspectFill
                        ? self.bodyView.frame.size.width - remoteListWidth
                        : halfBodyWidth,
                      height: self.bodyView.frame.size.height)
    }
    
    private func getCellSizeMapping(_ areaSize: CGSize) {
        self.cellSizeMapping.removeAll()
        self.gridCountMapping.removeAll()
        self.cachedPageSizes.removeAll()
        self.selectPageSize.removeAllItems()
        var set: Set<Int> = []
        // handle height/width > 1.8
        var waitList = [Int]()
        for i in 1...129 {
            let cellCount = i
            let layoutCount = self.cellCountMapping[cellCount]!
            let (size, totalCount) = self.calculateAspectFill(cellCount, layoutCount, areaSize)
            if size.height > size.width && size.height/size.width > 1.8 {
                waitList.append(cellCount)
                continue
            } else {
                waitList.append(i)
                for cellCount in waitList {
                    self.gridCountMapping[cellCount] = totalCount
                }
                waitList.removeAll()
            }
            set.insert(totalCount)
            self.cellSizeMapping[totalCount] = size
        }
        for item in Array(set).sorted() {
            if item * 2 > 100 {
                break
            } else if item > 1 {
                self.cachedPageSizes.append(item)
            }
        }
        // find selectIndex
        let pageSizes = Array(self.cachedPageSizes)
        self.selectPageSize.addItems(withTitles: pageSizes.map { "\($0)" })
        let oldSize = self.appContext.preferences.pageSize
        var selectIndex = pageSizes.firstIndex(of: oldSize)
        var preSizeRange = oldSize
        if selectIndex == nil {
            for i in 0..<pageSizes.count {
                let currSizeRange = oldSize - pageSizes[i]
                if currSizeRange < 0 {
                    if abs(currSizeRange) < preSizeRange {
                        selectIndex = i
                    } else {
                        selectIndex = i - 1
                    }
                    break
                } else {
                    preSizeRange = currSizeRange
                }
            }
        }
        self.selectPageSize.selectItem(at: selectIndex!)
    }
}
